/**
 * Copyright 2013-2020 the original author or authors from the JHipster project.
 *
 * This file is part of the JHipster project, see http://www.jhipster.tech/
 * for more information.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const JDLObject = require('../models/jdl-object');
const JDLEntity = require('../models/jdl-entity');
const JDLField = require('../models/jdl-field');
const JDLValidation = require('../models/jdl-validation');
const JDLEnum = require('../models/jdl-enum');
const JDLRelationship = require('../models/jdl-relationship');
const JDLUnaryOption = require('../models/jdl-unary-option');
const JDLBinaryOption = require('../models/jdl-binary-option');
const { CommonDBTypes } = require('../jhipster/field-types');
const { OptionNames } = require('../jhipster/application-options');
const { ONE_TO_ONE, ONE_TO_MANY, MANY_TO_ONE, MANY_TO_MANY } = require('../jhipster/relationship-types');
const { JPA_DERIVED_IDENTIFIER } = require('../jhipster/relationship-options');
const { FILTER, NO_FLUENT_METHOD, READ_ONLY, EMBEDDED } = require('../jhipster/unary-options');
const {
    ANGULAR_SUFFIX,
    CLIENT_ROOT_FOLDER,
    DTO,
    MICROSERVICE,
    PAGINATION,
    SEARCH,
    SERVICE,
} = require('../jhipster/binary-options').Options;
const { lowerFirst, upperFirst } = require('../utils/string-utils');

module.exports = {
    convertEntitiesToJDL,
};

const USER_ENTITY_NAME = 'User';
const USER_ENTITY = new JDLEntity({ name: USER_ENTITY_NAME });

let entities;
let jdlObject;
let skippedUserManagement;

/**
 * Convert the passed entities (parsed from JSON files) to a JDL object.
 * @param {Object} params - an object containing the entities and relevant options.
 * @param {Map<string, Object>} params.entities - a Map having for keys the entity names and values the JSON entity files.
 * @param {boolean} params.skippedUserManagement - whether management of the User entity by JHipster is skipped.
 * @return {JDLObject} the parsed entities in the JDL form.
 */
function convertEntitiesToJDL(params) {
    if (!params.entities) {
        throw new Error('Entities have to be passed to be converted.');
    }
    init(params);
    addEntities();
    addRelationshipsToJDL();
    return jdlObject;
}

function init(params) {
    entities = params.entities;
    jdlObject = new JDLObject();
    skippedUserManagement = params.skippedUserManagement;
}

function addEntities() {
    entities.forEach((entity, entityName) => {
        addEntity(entity, entityName);
    });
}

function addEntity(entity, entityName) {
    if (entityName === USER_ENTITY_NAME && !skippedUserManagement) {
        throw new Error(`User entity name is reserved if ${OptionNames.SKIP_USER_MANAGEMENT} is not set.`);
    }
    jdlObject.addEntity(convertJSONToJDLEntity(entity, entityName));
    addEnumsToJDL(entity);
    addEntityOptionsToJDL(entity, entityName);
}

function convertJSONToJDLEntity(entity, entityName) {
    const jdlEntity = new JDLEntity({
        name: entityName,
        tableName: entity.entityTableName,
        comment: entity.javadoc,
    });
    addFields(jdlEntity, entity);
    return jdlEntity;
}

function addFields(jdlEntity, entity) {
    entity.fields.forEach(field => {
        jdlEntity.addField(convertJSONToJDLField(field));
    });
}

function convertJSONToJDLField(field) {
    const jdlField = new JDLField({
        name: lowerFirst(field.fieldName),
        type: field.fieldType,
        comment: field.javadoc,
    });
    if (jdlField.type === 'byte[]') {
        jdlField.type = getTypeForBlob(field.fieldTypeBlobContent);
    }
    if (field.fieldValidateRules) {
        addValidations(jdlField, field);
    }
    return jdlField;
}

function getTypeForBlob(blobContentType) {
    if (['image', 'any', 'text'].includes(blobContentType)) {
        return CommonDBTypes[`${blobContentType.toUpperCase()}_BLOB`];
    }
    throw new Error(`Unrecognised blob type: '${blobContentType}'`);
}

function addValidations(jdlField, field) {
    field.fieldValidateRules.forEach(rule => {
        jdlField.addValidation(convertJSONToJDLValidation(rule, field));
    });
}

function convertJSONToJDLValidation(rule, field) {
    return new JDLValidation({
        name: rule,
        value: field[`fieldValidateRules${upperFirst(rule)}`],
    });
}

function addEnumsToJDL(entity) {
    entity.fields.forEach(field => {
        if (field.fieldValues !== undefined) {
            jdlObject.addEnum(
                new JDLEnum({
                    name: field.fieldType,
                    values: getEnumValuesFromString(field.fieldValues),
                })
            );
        }
    });
}

function getEnumValuesFromString(valuesAsString) {
    return valuesAsString.split(',').map(fieldValue => {
        // if fieldValue looks like ENUM_VALUE (something)
        if (fieldValue.includes('(')) {
            const [key, value] = fieldValue
                .replace(/^(\w+)\s\((\w+)\)$/, (match, matchedKey, matchedValue) => `${matchedKey},${matchedValue}`)
                .split(',');
            return {
                key,
                value,
            };
        }
        return { key: fieldValue };
    });
}

/*
 * Adds relationships for entities to JDL.
 * The jdl passed must contain the jdl entities concerned by the relationships
 */
function addRelationshipsToJDL() {
    entities.forEach((entity, entityName) => {
        dealWithRelationships(entity.relationships, entityName);
    });
}

function dealWithRelationships(relationships, entityName) {
    if (!relationships) {
        return;
    }
    relationships.forEach(relationship => {
        const jdlRelationship = getRelationship(relationship, entityName);
        if (jdlRelationship) {
            jdlObject.addRelationship(jdlRelationship);
        }
    });
}

function getRelationship(relationship, entityName) {
    let relationshipConfiguration = {
        sourceEntity: null,
        destinationEntity: null,
        type: null,
        injectedFieldInSourceEntity: null,
        injectedFieldInDestinationEntity: null,
        injectedFieldInDestinationIsRequired: false,
        injectedFieldInSourceIsRequired: false,
        commentForSourceEntity: null,
        commentForDestinationEntity: null,
        destinationEntityAsJDLEntity: null,
        options: {},
    };

    const sourceEntitySideAttributes = getSourceEntitySideAttributes(entityName, relationship);
    relationshipConfiguration = {
        ...relationshipConfiguration,
        ...sourceEntitySideAttributes,
    };

    relationshipConfiguration.options = getRelationshipOptions(relationship);

    if (shouldHandleUserEntity(relationship)) {
        relationshipConfiguration.destinationEntityAsJDLEntity = USER_ENTITY;
    } else {
        const destinationEntity = upperFirst(relationship.otherEntityName);
        relationshipConfiguration.destinationEntity = entities.get(destinationEntity);
        relationshipConfiguration.destinationEntityAsJDLEntity = jdlObject.getEntity(destinationEntity);
        if (!relationshipConfiguration.destinationEntity) {
            return undefined;
        }
        const isEntityTheDestinationSideEntity = (otherEntityName, otherEntityRelationshipName) =>
            otherEntityName === entityName && otherEntityRelationshipName === relationship.relationshipName;
        const destinationSideAttributes = getDestinationEntitySideAttributes(
            isEntityTheDestinationSideEntity,
            relationshipConfiguration.destinationEntity.relationships
        );
        relationshipConfiguration = {
            ...relationshipConfiguration,
            ...destinationSideAttributes,
        };
    }
    if (relationship.relationshipType === 'many-to-one') {
        if (relationshipConfiguration.injectedFieldInDestinationEntity) {
            // This is a bidirectional relationship so consider it as a OneToMany
            return getBidirectionalOneToManyRelationship(relationshipConfiguration);
        }
        // Unidirectional ManyToOne
        relationshipConfiguration.type = MANY_TO_ONE;
    } else if (relationship.relationshipType === 'one-to-one' && relationship.ownerSide === true) {
        relationshipConfiguration.type = ONE_TO_ONE;
    } else if (relationship.relationshipType === 'many-to-many' && relationship.ownerSide === true) {
        relationshipConfiguration.type = MANY_TO_MANY;
    }
    if (relationshipConfiguration.type) {
        return getDefaultRelationship(relationshipConfiguration);
    }
    return undefined;
}

function shouldHandleUserEntity(relationship) {
    return relationship.otherEntityName.toLowerCase() === USER_ENTITY_NAME.toLowerCase() && !skippedUserManagement;
}

function getSourceEntitySideAttributes(entityName, relationship) {
    return {
        sourceEntity: entityName,
        injectedFieldInSourceEntity: getInjectedFieldInSourceEntity(relationship),
        injectedFieldInSourceIsRequired: relationship.relationshipValidateRules,
        commentForSourceEntity: relationship.javadoc,
    };
}

function getDestinationEntitySideAttributes(isEntityTheDestinationSideEntity, destinationEntityRelationships) {
    const foundDestinationSideEntity = destinationEntityRelationships.find(destinationEntityFromRelationship => {
        return isEntityTheDestinationSideEntity(
            upperFirst(destinationEntityFromRelationship.otherEntityName),
            destinationEntityFromRelationship.otherEntityRelationshipName
        );
    });
    if (!foundDestinationSideEntity) {
        return {};
    }
    // Bidirectional relationship
    let injectedFieldInDestinationEntity = foundDestinationSideEntity.relationshipName;
    if (!!foundDestinationSideEntity.otherEntityField && foundDestinationSideEntity.otherEntityField !== 'id') {
        injectedFieldInDestinationEntity += `(${foundDestinationSideEntity.otherEntityField})`;
    }
    const injectedFieldInDestinationIsRequired = !!foundDestinationSideEntity.relationshipValidateRules;
    const commentForDestinationEntity = foundDestinationSideEntity.javadoc;
    return {
        injectedFieldInDestinationEntity,
        injectedFieldInDestinationIsRequired,
        commentForDestinationEntity,
    };
}

function getRelationshipOptions(relationship) {
    const options = relationship.options || {};
    if (relationship.useJPADerivedIdentifier) {
        options[JPA_DERIVED_IDENTIFIER] = true;
    }
    return options;
}

function getDefaultRelationship(relationshipConfiguration) {
    return new JDLRelationship({
        from: relationshipConfiguration.sourceEntity,
        to: relationshipConfiguration.destinationEntityAsJDLEntity.name,
        type: relationshipConfiguration.type,
        injectedFieldInFrom: relationshipConfiguration.injectedFieldInSourceEntity,
        injectedFieldInTo: relationshipConfiguration.injectedFieldInDestinationEntity,
        isInjectedFieldInFromRequired: relationshipConfiguration.injectedFieldInSourceIsRequired,
        isInjectedFieldInToRequired: relationshipConfiguration.injectedFieldInDestinationIsRequired,
        commentInFrom: relationshipConfiguration.commentForSourceEntity,
        commentInTo: relationshipConfiguration.commentForDestinationEntity,
        options: relationshipConfiguration.options,
    });
}

// Based on a ManyToOne relationship, we determine the OneToMany relationship.
function getBidirectionalOneToManyRelationship(relationshipConfiguration) {
    return new JDLRelationship({
        from: relationshipConfiguration.destinationEntityAsJDLEntity.name,
        to: relationshipConfiguration.sourceEntity,
        type: ONE_TO_MANY,
        injectedFieldInFrom: relationshipConfiguration.injectedFieldInDestinationEntity,
        injectedFieldInTo: relationshipConfiguration.injectedFieldInSourceEntity,
        isInjectedFieldInFromRequired: relationshipConfiguration.injectedFieldInDestinationIsRequired,
        isInjectedFieldInToRequired: relationshipConfiguration.injectedFieldInSourceIsRequired,
        commentInFrom: relationshipConfiguration.commentForDestinationEntity,
        commentInTo: relationshipConfiguration.commentForSourceEntity,
        options: relationshipConfiguration.options,
    });
}

function getInjectedFieldInSourceEntity(relationship) {
    return (
        relationship.relationshipName +
        (relationship.otherEntityField && relationship.otherEntityField !== 'id' ? `(${relationship.otherEntityField})` : '')
    );
}

function addEntityOptionsToJDL(entity, entityName) {
    if (entity.fluentMethods === false) {
        addUnaryOptionToJDL(NO_FLUENT_METHOD, entityName);
    }
    [DTO, PAGINATION, SERVICE].forEach(option => {
        if (entity[option] && entity[option] !== 'no') {
            addBinaryOptionToJDL(option, entity[option], entityName);
        }
    });
    if (entity.searchEngine) {
        addBinaryOptionToJDL(SEARCH, entity.searchEngine, entityName);
    }
    // angularSuffix in BinaryOptions, angularJSSuffix in Json
    if (entity.angularJSSuffix) {
        addBinaryOptionToJDL(ANGULAR_SUFFIX, entity.angularJSSuffix, entityName);
    }
    // microservice in BinaryOptions, microserviceName in Json
    if (entity.microserviceName !== undefined) {
        addBinaryOptionToJDL(MICROSERVICE, entity.microserviceName, entityName);
    }
    if (entity.jpaMetamodelFiltering === true) {
        addUnaryOptionToJDL(FILTER, entityName);
    }
    if (entity.readOnly === true) {
        addUnaryOptionToJDL(READ_ONLY, entityName);
    }
    if (entity.embedded === true) {
        addUnaryOptionToJDL(EMBEDDED, entityName);
    }
    if (entity.clientRootFolder) {
        addBinaryOptionToJDL(CLIENT_ROOT_FOLDER, entity.clientRootFolder, entityName);
    }
}

function addUnaryOptionToJDL(unaryOption, entityName) {
    jdlObject.addOption(
        new JDLUnaryOption({
            name: unaryOption,
            entityNames: [entityName],
        })
    );
}

function addBinaryOptionToJDL(binaryOption, value, entityName) {
    jdlObject.addOption(
        new JDLBinaryOption({
            name: binaryOption,
            value,
            entityNames: [entityName],
        })
    );
}
